我們知道,particle 是效果的基本單位,為了能夠達到我們所要的效果,我們必須調整每個 particle 的位置、顏色、大小等等屬性,所以在這裡我們需要定義一個 particle 會有甚麼屬性。
而我的 Particle 有以下屬性
struct Particle {
p2 pos;
p2 startVel;
p2 endVel;
p2 currentVel;
float currentSize, startSize, endSize;
float angle;
float startRotSpeed;
float currentRotSpeed;
p4 startColor;
p4 currentColor;
p4 endColor;
float t; // Timestep
uint32_t startLife;
uint32_t life;
bool draw = false;
Particle() = default;
};
我們有很多的 startXXX
跟 endXXX
,上面也解釋說是產生時到死亡時的變化,例如我的 startColor
設定成白色,endColor
設定成黑色,那我的 Particle 在生命週期開始的時候會是白色,並且利用差值的方式,漸漸地由白色轉呈黑色,最後在生命週期到的時候變成黑色然後消失。大小、速度也是一樣的道理。
如果不是用ECS架構的話,我的 Particle 會是一個 class,Emitter 也會是一個 class,不過應和架構,我的 Emitter 也只會有 Data。而主要激活 Particle 的就由 Particle System
來做。
那我們知道 Emitter 是定義 Particle 行為的地方,意思是說,如果我希望我的效果會讓粒子從紫色變黃色,我就要告訴 Emitter 我希望我的起始顏色是紫色,結束顏色是黃色。接著當遊戲循環開始時,Emitter就會根據你給的數據,將他給予到每個 Particle,讓他們模擬。
如果今天我希望有另一個效果可以從紅色變藍色,我只需要在產生一個新的 Emitter 去存取新的數據,這樣就可以有新的效果了。
聽起來 Emitter 的數據跟 Particle 很像。
答對了~~
struct ParticleComponent {
// Emitter排放角度,0~360度就會是圓形排放
p2 angleRange = {0.f, 360.f};
// Particle 產生時的速度
float startSpeed = 0.f;
// Particle 死亡時的速度
float endSpeed = 0.f;
// Particle 產生時的size
float startSize = 0.f;
// Particle 死亡時的size
float endSize = 0.f;
// Particle Texture Rotate的速度
float rotateSpeed = 0.f;
// 每個frame產生Particle的數量,由emitNumber跟emitVariance算出
int emissionRate = 0; // new Particles
// 每個frame保底產生Particle的數量
uint32_t emitNumber = 0;
// 讓每個frame產生的數量不固定。
uint32_t emitVariance = 0;
// 每個Particle的life
uint32_t maxParticleLife = 0;
// 用來計算pool size
uint32_t maxParticlesPerFrame = 0;
int poolSize;
// Emitter 是死是活
bool active = false;
// Emitter 的 Life
float life = -1;
// 每 sleepTime 產生一次Particle
float sleepTime;
Timer lifeTimer;
Timer sleepTimer;
// Particle 產生時的顏色
p4 startColor = {0, 0, 0, 0};
// Particle 死亡時的顏色
p4 endColor = {0, 0, 0, 0};
// Random control
// XXRand 代表 XX 的random範圍,讓Particle看起來不會那麼整齊
p2 rotSpeedRand = {0.f, 0.f};
p2 startSpeedRand = { 0.f, 0.f};
p2 endSpeedRand = {0.f, 0.f};
p2 emitVarianceRand = {0.f, 0.f};
p2 startSizeRand = {0.f, 0.f};
p2 endSizeRand = {0.f, 0.f};
p2 lifeRand = {0.f, 0.f};
// disX, disY 用來決定paritcle產生的位置範圍,
// disX = 100 代表距離emitter pos center的 +- 100內都會產生particle
float disX = 0.f;
float disY = 0.f;
// 產生每個particle的時候要找pool中尚未被激活 或者死亡的particle激活
int lastUnusedParticle = 0;
std::shared_ptr<Texture2D> texture;
// Particle pool
std::vector<Particle> particles;
EmitterComponent(std::shared_ptr<Texture2D> texture) : texture(texture){}
};
這裡我提一下 Particle沒有的數據
-1
代表永久活著100 * rand(0, 1)
,這樣才不會讓效果太過單調。std::vector<Particle> particles
;
我希望我所有的資料是存在 Json 檔裡的,像這樣
{
"value0": {
"angleRange": {
"x": 250.0,
"y": 280.0
},
"startSpeed": 200.0,
"endSpeed": 200.0,
"startSize": 0.0,
"endSize": 80.0,
"rotateSpeed": 0.0,
"emitNumber": 3,
"emitVariance": 2,
"maxParticleLife": 80,
"life": -1.0,
"sleepTime": -1.0,
"startColor": {
"x": 1.0,
"y": 0.3921568691730499,
"z": 0.0,
"w": 1.0
},
"endColor": {
"x": 0.8235294222831726,
"y": 0.8235294222831726,
"z": 0.8235294222831726,
"w": 0.0
},
...
}
}
從檔案讀取之後,將資料輸入至 Emitter,再利用 ParticleSystem 將所有的 Emitter 進行模擬。
而我們的 EmitData 所要放的東西就是我們能夠更改的一些 Data,像 emissionRate 這種資料是利用其他資料算出來的,就不用在 EmitData 裡。
struct EmitterData {
p2 angleRange = {0.f, 0.f};
// Particle Speed
float startSpeed = 0.f;
float endSpeed = 0.f;
float startSize = 0.f;
float endSize = 0.f;
// Particle Texture rotate speed #unused now
float rotateSpeed = 0.f;
// Controll emit rate
uint32_t emitNumber = 0; // Generate per frame
uint32_t emitVariance = 0; // Offset of random
uint32_t maxParticleLife = 0; // particle life
// Control emitter life time
float life = -1;
float sleepTime = -1;
// Particle Color
p4 startColor = {1.0, 0.0, 0.0, 1};
p4 endColor = {0.0, 0.0, 1., 0};
// Random control
p2 rotSpeedRand = {-1.f, 1.f};
p2 startSpeedRand = { 0.f, 1.f};
p2 endSpeedRand = {0.f, 1.f};
p2 emitVarianceRand = {0.f, 1.f};
p2 startSizeRand = {0.f, 1.f};
p2 endSizeRand = {1.f, 1.f};
p2 lifeRand = {0.5f, 1.f};
// Different pos init
float disX = 0.f;
float disY = 0.f;
};
而將資料存成 Json file 我是用 cereal
這個 library。
而將 System 寫好後的 code 會大概長這樣
// 將資料讀進
std::ifstream is("Fire.json");
std::string str((std::istreambuf_iterator<char>(is)), std::istreambuf_iterator<char>());
std::stringstream oos(str);
cereal::JSONInputArchive ar(oos);
EmitterData data;
ar(data);
////////////////////
// 產生一個 Entity
auto emit = registry.create();
// 添加 Transform,也就是讓他有 pos,並且設定位置在目前劃屬位置
registry.emplace<TransformComponent>(emit, glm::vec2(sf::Mouse::getPosition(window_).x, sf::Mouse::getPosition(window_).y), glm::vec2(100.f, 100.f));
// 添加 Particle效果
registry.emplace<ParticleComponent>(emit, ResourceManager::getTexture("5"));
registry.emplace<TagComponent>(emit);
// 將資料輸入至 Emitter
particleSystem.initEmitter(registry, emit, data);